﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using K9Nano.Authorization;
using K9Nano.Configuration;
using K9Nano.Configuration.Startup;
using K9Nano.Dependency;
using K9Nano.Domain;
using K9Nano.Events.Entity;
using K9Nano.MultiTenancy;
using K9Nano.Runtime;
using K9Nano.Share.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace K9Nano.EFCore
{
    public class DbContextHelper : IScopedDependency, IDbContextHelper
    {
        public DbContextHelper(IMultiTenancyConfig multiTenancyConfig,
            ISessionContext sessionContext,
            IGlobalFilterProvider globalFilterProvider,
            IEntityChangeReport entityChangeReport,
            ISystemClock systemClock,
            IEntityCacheFilter entityCacheFilter,
            IEntityEventFilter entityEventFilter)
        {
            SessionContext = sessionContext;
            GlobalFilterProvider = globalFilterProvider;
            EntityChangeReport = entityChangeReport;
            SystemClock = systemClock;
            EntityCacheFilter = entityCacheFilter;
            EntityEventFilter = entityEventFilter;
            MultiTenancy = multiTenancyConfig.IsEnabled;
        }

        #region Properties

        public DbContext DbContext { get; set; }

        public ISessionContext SessionContext { get; }

        public IGlobalFilterProvider GlobalFilterProvider { get; }

        public IEntityChangeReport EntityChangeReport { get; }

        public ISystemClock SystemClock { get; }

        public IEntityCacheFilter EntityCacheFilter { get; }

        public IEntityEventFilter EntityEventFilter { get; }

        public virtual bool MultiTenancy { get; }

        public virtual bool FiltersDisabled { get; set; } = false;

        public virtual bool SuppressAutoSetTenantId { get; set; } = false;

        public virtual bool EventsDisabled { get; set; } = false;

        #endregion

        #region ModelCreating

        public void OnModelCreating(ModelBuilder modelBuilder)
        {
            var settingBuilder = modelBuilder.Entity<AppSetting>();
            settingBuilder.Property(a => a.Name).HasMaxLength(255);
            settingBuilder.Property(a => a.Value).HasMaxLength(255);
            settingBuilder.HasIndex(a => new { a.TenantId, a.UserId }).HasName("idx_tenant_user");

            var tenantBuilder = modelBuilder.Entity<Tenant>();
            tenantBuilder.Property(x => x.Name).HasMaxLength(255);
            tenantBuilder.HasIndex(x => x.Name).IsUnique().HasName("idx_name");
            tenantBuilder.Property(x => x.DisplayName).HasMaxLength(255);

            var roleResBuilder = modelBuilder.Entity<RoleResource>();

            var auditLogBuilder = modelBuilder.Entity<AuditLog>();
            
            if (MultiTenancy)
            {
                roleResBuilder.HasIndex(a => new { a.TenantId, a.Api, a.Role, a.Resource })
                    .HasName("idx_tenant_role_res");

                auditLogBuilder.HasIndex(a => new { a.TenantId, a.UserId })
                    .HasName("idx_tenant_user");
                auditLogBuilder.HasIndex(a => new { a.TenantId, a.AuditTime })
                    .HasName("idx_tenant_time");
            }
            else
            {
                roleResBuilder.HasIndex(a => new { a.Api, a.Role, a.Resource })
                    .HasName("idx_role_res");

                auditLogBuilder.HasIndex(a => a.UserId)
                    .HasName("idx_user");
                auditLogBuilder.HasIndex(a => a.AuditTime)
                    .HasName("idx_time");
            }

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                // Global filters
                this.GlobalFilterProvider.ConfigureGlobalFiltersByType(entityType.ClrType, modelBuilder, entityType);

                // Naming convention
                var entityBuilder = modelBuilder.Entity(entityType.Name);
                var currentTableName = entityBuilder.Metadata.GetTableName();
                entityBuilder.ToTable(GetConventionalTableName(currentTableName));
            }
        }

        #endregion

        #region Naming Convention

        /// <summary>
        /// Get conventional table name
        /// </summary>
        /// <param name="tableName"></param>
        /// <returns></returns>
        public virtual string GetConventionalTableName(string tableName)
        {
            return tableName.ToSingular().ToSnakeCase();
        }

        #endregion

        #region SaveChanges

        public virtual void SetValueIfNull(EntityEntry entry, string key, object value)
        {
            var column = entry.CurrentValues[key];
            if (column == null)
            {
                entry.CurrentValues[key] = value;
            }
            else
            {
                var columnType = column.GetType();

                if (columnType.IsValueType && column != value)
                {
                    entry.CurrentValues[key] = value;
                }
            }
        }

        public virtual int SaveChanges(bool acceptAllChangesOnSuccess, Func<bool, int> baseSaveChanges)
        {
            var reports = BeforeSaveChanges();
            var result = baseSaveChanges(acceptAllChangesOnSuccess);
            EntityChangeReport.Trigger(reports);
            return result;
        }

        public virtual async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
            CancellationToken cancellation,
            Func<bool, CancellationToken, Task<int>> baseSaveChanges)
        {
            var reports = BeforeSaveChanges();
            var result = await baseSaveChanges(acceptAllChangesOnSuccess, cancellation);
            await EntityChangeReport.TriggerAsync(reports);
            return result;
        }

        private List<EntityChange> BeforeSaveChanges()
        {
            var entries = DbContext.ChangeTracker.Entries();
            var entityChanges = new List<EntityChange>();
            foreach (var entry in entries)
            {
                EEntityChangeType changeType;
                switch (entry.State)
                {
                    case EntityState.Added:
                        if (MultiTenancy && !SuppressAutoSetTenantId)
                        {
                            if (entry.Entity is IMustHaveTenant)
                            {
                                SetValueIfNull(entry, nameof(IMustHaveTenant.TenantId), SessionContext.RequiredTenantId);
                            }
                            else if (entry.Entity is IMayHaveTenant)
                            {
                                SetValueIfNull(entry, nameof(IMayHaveTenant.TenantId), SessionContext.TenantId);
                            }
                        }

                        if (entry.Entity is ICreationAudited)
                        {
                            SetValueIfNull(entry, nameof(ICreationAudited.CreationTime), SystemClock.UtcNow);
                            SetValueIfNull(entry, nameof(ICreationAudited.CreatorUserId), SessionContext.UserId);
                        }

                        changeType = EEntityChangeType.Created;

                        break;
                    case EntityState.Modified:
                        if (entry.Entity is IModificationAudited)
                        {
                            SetValueIfNull(entry, nameof(IModificationAudited.LastModificationTime), SystemClock.UtcNow);
                            SetValueIfNull(entry, nameof(IModificationAudited.LastModifierUserId), SessionContext.UserId);
                        }

                        changeType = EEntityChangeType.Updated;
                        break;
                    case EntityState.Deleted:
                        if (entry.Entity is IDeletionAudited)
                        {
                            SetValueIfNull(entry, nameof(IDeletionAudited.DeleterUserId), SessionContext.UserId);
                            SetValueIfNull(entry, nameof(IDeletionAudited.DeletionTime), SystemClock.UtcNow);
                            entry.State = EntityState.Modified;
                        }
                        changeType = EEntityChangeType.Deleted;
                        break;
                    default:
                        continue;
                }

                // disable Events and Cache when bulking CUD?
                var enableEvent = !EventsDisabled && EntityEventFilter.Propagate(entry.Entity);
                var enableCache = EntityCacheFilter.Cache(entry.Entity);

                if (enableCache || enableEvent)
                {
                    var id = changeType == EEntityChangeType.Created ? null : entry.CurrentValues["Id"];
                    entityChanges.Add(new EntityChange(changeType, entry.Entity, id, enableEvent, enableCache));
                }
            }

            return entityChanges;
        }

        #endregion

        public virtual IQueryable<TEntity> Queryable<TEntity>() where TEntity : class
        {
            return FiltersDisabled ? DbContext.Set<TEntity>().IgnoreQueryFilters() : DbContext.Set<TEntity>().AsQueryable();
        }
    }
}